Passed
Branch wavefile-reader (133a0d)
by Rafael S.
02:20
created

WaveFileParser.getLtxtChunkBytes_   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 13
rs 9.8
c 0
b 0
f 0
cc 1
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileParser class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import WaveFileReader from './wavefile-reader';
33
import writeString from './write-string';
34
import validateNumChannels from './validate-num-channels'; 
35
import validateSampleRate from './validate-sample-rate';
36
import {unpack, packTo, packStringTo, packString, pack} from 'byte-data';
37
38
/**
39
 * A class to read and write wav files.
40
 * @extends WaveFileReader
41
 */
42
export default class WaveFileParser extends WaveFileReader {
43
44
  constructor(wavBuffer=null) {
45
    super(wavBuffer);
46
    /**
47
     * Audio formats.
48
     * Formats not listed here should be set to 65534,
49
     * the code for WAVE_FORMAT_EXTENSIBLE
50
     * @enum {number}
51
     * @protected
52
     */
53
    this.WAV_AUDIO_FORMATS = {
54
      '4': 17,
55
      '8': 1,
56
      '8a': 6,
57
      '8m': 7,
58
      '16': 1,
59
      '24': 1,
60
      '32': 1,
61
      '32f': 3,
62
      '64': 3
63
    };
64
  }
65
66
  /**
67
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
68
   * The return value of this method can be written straight to disk.
69
   * @return {!Uint8Array} A wav file.
70
   * @throws {Error} If bit depth is invalid.
71
   * @throws {Error} If the number of channels is invalid.
72
   * @throws {Error} If the sample rate is invalid.
73
   */
74
  toBuffer() {
75
    this.validateWavHeader();
76
    return this.writeWavBuffer_();
77
  }
78
79
  /**
80
   * Return the sample at a given index.
81
   * @param {number} index The sample index.
82
   * @return {number} The sample.
83
   * @throws {Error} If the sample index is off range.
84
   */
85
  getSample(index) {
86
    index = index * (this.dataType.bits / 8);
87
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
88
      throw new Error('Range error');
89
    }
90
    return unpack(
91
      this.data.samples.slice(index, index + this.dataType.bits / 8),
92
      this.dataType);
93
  }
94
95
  /**
96
   * Set the sample at a given index.
97
   * @param {number} index The sample index.
98
   * @param {number} sample The sample.
99
   * @throws {Error} If the sample index is off range.
100
   */
101
  setSample(index, sample) {
102
    index = index * (this.dataType.bits / 8);
103
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
104
      throw new Error('Range error');
105
    }
106
    packTo(sample, this.dataType, this.data.samples, index);
107
  }
108
109
  /**
110
   * Validate the header of the file.
111
   * @throws {Error} If bit depth is invalid.
112
   * @throws {Error} If the number of channels is invalid.
113
   * @throws {Error} If the sample rate is invalid.
114
   * @ignore
115
   * @protected
116
   */
117
  validateWavHeader() {
118
    this.validateBitDepth_();
119
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
120
      throw new Error('Invalid number of channels.');
121
    }
122
    if (!validateSampleRate(
123
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
124
      throw new Error('Invalid sample rate.');
125
    }
126
  }
127
128
  /**
129
   * Return a .wav file byte buffer with the data from the WaveFileParser object.
130
   * The return value of this method can be written straight to disk.
131
   * @return {!Uint8Array} The wav file bytes.
132
   * @private
133
   */
134
  writeWavBuffer_() {
135
    this.uInt16.be = this.container === 'RIFX';
136
    this.uInt32.be = this.uInt16.be;
137
    /** @type {!Array<!Array<number>>} */
138
    let fileBody = [
139
      this.getJunkBytes_(),
140
      this.getDs64Bytes_(),
141
      this.getBextBytes_(),
142
      this.getFmtBytes_(),
143
      this.getFactBytes_(),
144
      packString(this.data.chunkId),
145
      pack(this.data.samples.length, this.uInt32),
146
      this.data.samples,
147
      this.getCueBytes_(),
148
      this.getSmplBytes_(),
149
      this.getLISTBytes_()
150
    ];
151
    /** @type {number} */
152
    let fileBodyLength = 0;
153
    for (let i=0; i<fileBody.length; i++) {
154
      fileBodyLength += fileBody[i].length;
155
    }
156
    /** @type {!Uint8Array} */
157
    let file = new Uint8Array(fileBodyLength + 12);
158
    /** @type {number} */
159
    let index = 0;
160
    index = packStringTo(this.container, file, index);
161
    index = packTo(fileBodyLength + 4, this.uInt32, file, index);
162
    index = packStringTo(this.format, file, index);
163
    for (let i=0; i<fileBody.length; i++) {
164
      file.set(fileBody[i], index);
165
      index += fileBody[i].length;
166
    }
167
    return file;
168
  }
169
170
  /**
171
   * Return the bytes of the 'bext' chunk.
172
   * @private
173
   */
174
  getBextBytes_() {
175
    /** @type {!Array<number>} */
176
    let bytes = [];
177
    this.enforceBext_();
178
    if (this.bext.chunkId) {
179
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
180
      bytes = bytes.concat(
181
        packString(this.bext.chunkId),
182
        pack(602 + this.bext.codingHistory.length, this.uInt32),
183
        writeString(this.bext.description, 256),
184
        writeString(this.bext.originator, 32),
185
        writeString(this.bext.originatorReference, 32),
186
        writeString(this.bext.originationDate, 10),
187
        writeString(this.bext.originationTime, 8),
188
        pack(this.bext.timeReference[0], this.uInt32),
189
        pack(this.bext.timeReference[1], this.uInt32),
190
        pack(this.bext.version, this.uInt16),
191
        writeString(this.bext.UMID, 64),
192
        pack(this.bext.loudnessValue, this.uInt16),
193
        pack(this.bext.loudnessRange, this.uInt16),
194
        pack(this.bext.maxTruePeakLevel, this.uInt16),
195
        pack(this.bext.maxMomentaryLoudness, this.uInt16),
196
        pack(this.bext.maxShortTermLoudness, this.uInt16),
197
        writeString(this.bext.reserved, 180),
198
        writeString(
199
          this.bext.codingHistory, this.bext.codingHistory.length));
200
    }
201
    return bytes;
202
  }
203
204
  /**
205
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
206
   * @private
207
   */
208
  enforceBext_() {
209
    for (let prop in this.bext) {
210
      if (this.bext.hasOwnProperty(prop)) {
211
        if (this.bext[prop] && prop != 'timeReference') {
212
          this.bext.chunkId = 'bext';
213
          break;
214
        }
215
      }
216
    }
217
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
218
      this.bext.chunkId = 'bext';
219
    }
220
  }
221
222
  /**
223
   * Return the bytes of the 'ds64' chunk.
224
   * @return {!Array<number>} The 'ds64' chunk bytes.
225
   * @private
226
   */
227
  getDs64Bytes_() {
228
    /** @type {!Array<number>} */
229
    let bytes = [];
230
    if (this.ds64.chunkId) {
231
      bytes = bytes.concat(
232
        packString(this.ds64.chunkId),
233
        pack(this.ds64.chunkSize, this.uInt32),
234
        pack(this.ds64.riffSizeHigh, this.uInt32),
235
        pack(this.ds64.riffSizeLow, this.uInt32),
236
        pack(this.ds64.dataSizeHigh, this.uInt32),
237
        pack(this.ds64.dataSizeLow, this.uInt32),
238
        pack(this.ds64.originationTime, this.uInt32),
239
        pack(this.ds64.sampleCountHigh, this.uInt32),
240
        pack(this.ds64.sampleCountLow, this.uInt32));
241
    }
242
    //if (this.ds64.tableLength) {
243
    //  ds64Bytes = ds64Bytes.concat(
244
    //    pack(this.ds64.tableLength, this.uInt32),
245
    //    this.ds64.table);
246
    //}
247
    return bytes;
248
  }
249
250
  /**
251
   * Return the bytes of the 'cue ' chunk.
252
   * @return {!Array<number>} The 'cue ' chunk bytes.
253
   * @private
254
   */
255
  getCueBytes_() {
256
    /** @type {!Array<number>} */
257
    let bytes = [];
258
    if (this.cue.chunkId) {
259
      /** @type {!Array<number>} */
260
      let cuePointsBytes = this.getCuePointsBytes_();
261
      bytes = bytes.concat(
262
        packString(this.cue.chunkId),
263
        pack(cuePointsBytes.length + 4, this.uInt32),
264
        pack(this.cue.dwCuePoints, this.uInt32),
265
        cuePointsBytes);
266
    }
267
    return bytes;
268
  }
269
270
  /**
271
   * Return the bytes of the 'cue ' points.
272
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
273
   * @private
274
   */
275
  getCuePointsBytes_() {
276
    /** @type {!Array<number>} */
277
    let points = [];
278
    for (let i=0; i<this.cue.dwCuePoints; i++) {
279
      points = points.concat(
280
        pack(this.cue.points[i].dwName, this.uInt32),
281
        pack(this.cue.points[i].dwPosition, this.uInt32),
282
        packString(this.cue.points[i].fccChunk),
283
        pack(this.cue.points[i].dwChunkStart, this.uInt32),
284
        pack(this.cue.points[i].dwBlockStart, this.uInt32),
285
        pack(this.cue.points[i].dwSampleOffset, this.uInt32));
286
    }
287
    return points;
288
  }
289
290
  /**
291
   * Return the bytes of the 'smpl' chunk.
292
   * @return {!Array<number>} The 'smpl' chunk bytes.
293
   * @private
294
   */
295
  getSmplBytes_() {
296
    /** @type {!Array<number>} */
297
    let bytes = [];
298
    if (this.smpl.chunkId) {
299
      /** @type {!Array<number>} */
300
      let smplLoopsBytes = this.getSmplLoopsBytes_();
301
      bytes = bytes.concat(
302
        packString(this.smpl.chunkId),
303
        pack(smplLoopsBytes.length + 36, this.uInt32),
304
        pack(this.smpl.dwManufacturer, this.uInt32),
305
        pack(this.smpl.dwProduct, this.uInt32),
306
        pack(this.smpl.dwSamplePeriod, this.uInt32),
307
        pack(this.smpl.dwMIDIUnityNote, this.uInt32),
308
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32),
309
        pack(this.smpl.dwSMPTEFormat, this.uInt32),
310
        pack(this.smpl.dwSMPTEOffset, this.uInt32),
311
        pack(this.smpl.dwNumSampleLoops, this.uInt32),
312
        pack(this.smpl.dwSamplerData, this.uInt32),
313
        smplLoopsBytes);
314
    }
315
    return bytes;
316
  }
317
318
  /**
319
   * Return the bytes of the 'smpl' loops.
320
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
321
   * @private
322
   */
323
  getSmplLoopsBytes_() {
324
    /** @type {!Array<number>} */
325
    let loops = [];
326
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
327
      loops = loops.concat(
328
        pack(this.smpl.loops[i].dwName, this.uInt32),
329
        pack(this.smpl.loops[i].dwType, this.uInt32),
330
        pack(this.smpl.loops[i].dwStart, this.uInt32),
331
        pack(this.smpl.loops[i].dwEnd, this.uInt32),
332
        pack(this.smpl.loops[i].dwFraction, this.uInt32),
333
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32));
334
    }
335
    return loops;
336
  }
337
338
  /**
339
   * Return the bytes of the 'fact' chunk.
340
   * @return {!Array<number>} The 'fact' chunk bytes.
341
   * @private
342
   */
343
  getFactBytes_() {
344
    /** @type {!Array<number>} */
345
    let bytes = [];
346
    if (this.fact.chunkId) {
347
      bytes = bytes.concat(
348
        packString(this.fact.chunkId),
349
        pack(this.fact.chunkSize, this.uInt32),
350
        pack(this.fact.dwSampleLength, this.uInt32));
351
    }
352
    return bytes;
353
  }
354
355
  /**
356
   * Return the bytes of the 'fmt ' chunk.
357
   * @return {!Array<number>} The 'fmt' chunk bytes.
358
   * @throws {Error} if no 'fmt ' chunk is present.
359
   * @private
360
   */
361
  getFmtBytes_() {
362
    /** @type {!Array<number>} */
363
    let fmtBytes = [];
364
    if (this.fmt.chunkId) {
365
      return fmtBytes.concat(
366
        packString(this.fmt.chunkId),
367
        pack(this.fmt.chunkSize, this.uInt32),
368
        pack(this.fmt.audioFormat, this.uInt16),
369
        pack(this.fmt.numChannels, this.uInt16),
370
        pack(this.fmt.sampleRate, this.uInt32),
371
        pack(this.fmt.byteRate, this.uInt32),
372
        pack(this.fmt.blockAlign, this.uInt16),
373
        pack(this.fmt.bitsPerSample, this.uInt16),
374
        this.getFmtExtensionBytes_());
375
    }
376
    throw Error('Could not find the "fmt " chunk');
377
  }
378
379
  /**
380
   * Return the bytes of the fmt extension fields.
381
   * @return {!Array<number>} The fmt extension bytes.
382
   * @private
383
   */
384
  getFmtExtensionBytes_() {
385
    /** @type {!Array<number>} */
386
    let extension = [];
387
    if (this.fmt.chunkSize > 16) {
388
      extension = extension.concat(
389
        pack(this.fmt.cbSize, this.uInt16));
390
    }
391
    if (this.fmt.chunkSize > 18) {
392
      extension = extension.concat(
393
        pack(this.fmt.validBitsPerSample, this.uInt16));
394
    }
395
    if (this.fmt.chunkSize > 20) {
396
      extension = extension.concat(
397
        pack(this.fmt.dwChannelMask, this.uInt32));
398
    }
399
    if (this.fmt.chunkSize > 24) {
400
      extension = extension.concat(
401
        pack(this.fmt.subformat[0], this.uInt32),
402
        pack(this.fmt.subformat[1], this.uInt32),
403
        pack(this.fmt.subformat[2], this.uInt32),
404
        pack(this.fmt.subformat[3], this.uInt32));
405
    }
406
    return extension;
407
  }
408
409
  /**
410
   * Return the bytes of the 'LIST' chunk.
411
   * @return {!Array<number>} The 'LIST' chunk bytes.
412
   * @private
413
   */
414
  getLISTBytes_() {
415
    /** @type {!Array<number>} */
416
    let bytes = [];
417
    for (let i=0; i<this.LIST.length; i++) {
418
      /** @type {!Array<number>} */
419
      let subChunksBytes = this.getLISTSubChunksBytes_(
420
          this.LIST[i].subChunks, this.LIST[i].format);
421
      bytes = bytes.concat(
422
        packString(this.LIST[i].chunkId),
423
        pack(subChunksBytes.length + 4, this.uInt32),
424
        packString(this.LIST[i].format),
425
        subChunksBytes);
426
    }
427
    return bytes;
428
  }
429
430
  /**
431
   * Return the bytes of the sub chunks of a 'LIST' chunk.
432
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
433
   * @param {string} format The format of the 'LIST' chunk.
434
   *    Currently supported values are 'adtl' or 'INFO'.
435
   * @return {!Array<number>} The sub chunk bytes.
436
   * @private
437
   */
438
  getLISTSubChunksBytes_(subChunks, format) {
439
    /** @type {!Array<number>} */
440
    let bytes = [];
441
    for (let i=0; i<subChunks.length; i++) {
442
      if (format == 'INFO') {
443
        bytes = bytes.concat(
444
          packString(subChunks[i].chunkId),
445
          pack(subChunks[i].value.length + 1, this.uInt32),
446
          writeString(
447
            subChunks[i].value, subChunks[i].value.length));
448
        bytes.push(0);
449
      } else if (format == 'adtl') {
450
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
451
          bytes = bytes.concat(
452
            packString(subChunks[i].chunkId),
453
            pack(
454
              subChunks[i].value.length + 4 + 1, this.uInt32),
455
            pack(subChunks[i].dwName, this.uInt32),
456
            writeString(
457
              subChunks[i].value,
458
              subChunks[i].value.length));
459
          bytes.push(0);
460
        } else if (subChunks[i].chunkId == 'ltxt') {
461
          bytes = bytes.concat(
462
            this.getLtxtChunkBytes_(subChunks[i]));
463
        }
464
      }
465
      if (bytes.length % 2) {
466
        bytes.push(0);
467
      }
468
    }
469
    return bytes;
470
  }
471
472
  /**
473
   * Return the bytes of a 'ltxt' chunk.
474
   * @param {!Object} ltxt the 'ltxt' chunk.
475
   * @private
476
   */
477
  getLtxtChunkBytes_(ltxt) {
478
    return [].concat(
479
      packString(ltxt.chunkId),
480
      pack(ltxt.value.length + 20, this.uInt32),
481
      pack(ltxt.dwName, this.uInt32),
482
      pack(ltxt.dwSampleLength, this.uInt32),
483
      pack(ltxt.dwPurposeID, this.uInt32),
484
      pack(ltxt.dwCountry, this.uInt16),
485
      pack(ltxt.dwLanguage, this.uInt16),
486
      pack(ltxt.dwDialect, this.uInt16),
487
      pack(ltxt.dwCodePage, this.uInt16),
488
      writeString(ltxt.value, ltxt.value.length));
489
  }
490
491
  /**
492
   * Return the bytes of the 'junk' chunk.
493
   * @private
494
   */
495
  getJunkBytes_() {
496
    /** @type {!Array<number>} */
497
    let bytes = [];
498
    if (this.junk.chunkId) {
499
      return bytes.concat(
500
        packString(this.junk.chunkId),
501
        pack(this.junk.chunkData.length, this.uInt32),
502
        this.junk.chunkData);
503
    }
504
    return bytes;
505
  }
506
507
  /**
508
   * Validate the bit depth.
509
   * @return {boolean} True is the bit depth is valid.
510
   * @throws {Error} If bit depth is invalid.
511
   * @private
512
   */
513
  validateBitDepth_() {
514
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
515
      if (parseInt(this.bitDepth, 10) > 8 &&
516
          parseInt(this.bitDepth, 10) < 54) {
517
        return true;
518
      }
519
      throw new Error('Invalid bit depth.');
520
    }
521
    return true;
522
  }
523
}
524